!(function() {
  'use strict';

  L.Marker.Measurement = L[L.Layer ? 'Layer' : 'Class'].extend({
    options: {
      pane: 'markerPane'
    },

    initialize: function(latlng, measurement, title, rotation, options) {
      L.setOptions(this, options);

      this._latlng = latlng;
      this._measurement = measurement;
      this._title = title;
      this._rotation = rotation;
    },

    addTo: function(map) {
      map.addLayer(this);
      return this;
    },

    onAdd: function(map) {
      this._map = map;
      var pane = this.getPane ? this.getPane() : map.getPanes().markerPane;
      var el = this._element = L.DomUtil.create('div', 'leaflet-zoom-animated leaflet-leaflet-measure-path-measurement', pane);
      var inner = L.DomUtil.create('div', '', el);
      inner.title = this._title;
      inner.innerHTML = this._measurement;

      map.on('zoomanim', this._animateZoom, this);

      this._setPosition();
    },

    onRemove: function(map) {
      map.off('zoomanim', this._animateZoom, this);
      var pane = this.getPane ? this.getPane() : map.getPanes().markerPane;
      pane.removeChild(this._element);
      this._map = null;
    },

    _setPosition: function() {
      L.DomUtil.setPosition(this._element, this._map.latLngToLayerPoint(this._latlng));
      this._element.style.transform += ' rotate(' + this._rotation + 'rad)';
    },

    _animateZoom: function(opt) {
      var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center).round();
      L.DomUtil.setPosition(this._element, pos);
      this._element.style.transform += ' rotate(' + this._rotation + 'rad)';
    }
  });

  L.marker.measurement = function(latLng, measurement, title, rotation, options) {
    return new L.Marker.Measurement(latLng, measurement, title, rotation, options);
  };

  var formatDistance = function(d) {
    var unit,
      feet;

    if (this._measurementOptions.imperial) {
      feet = d / 0.3048;
      if (feet > 3000) {
        d = d / 1609.344;
        unit = 'mi';
      } else {
        d = feet;
        unit = 'ft';
      }
    } else {
      if (d > 1000) {
        d = d / 1000;
        unit = 'km';
      } else {
        unit = 'm';
      }
    }

    if (d < 100) {
      return d.toFixed(1) + ' ' + unit;
    } else {
      return Math.round(d) + ' ' + unit;
    }
  }

  var formatArea = function(a) {
    var unit,
      sqfeet;

    if (this._measurementOptions.imperial) {
      if (a > 404.685642) {
        a = a / 4046.85642;
        unit = 'ac';
      } else {
        a = a / 0.09290304;
        unit = 'ft²';
      }
    } else if (this._measurementOptions.ha) {
      if (a > 1000000000) {
        a = a / 1000000000;
        unit = 'km²';
      } else if (a > 10000) {
        a = a / 10000;
        unit = 'ha';
      } else {
        unit = 'm²';
      }
    } else {
      if (a > 1000000) {
        a = a / 1000000;
        unit = 'km²';
      } else {
        unit = 'm²';
      }
    }

    if (a < 100) {
      return a.toFixed(1) + ' ' + unit;
    } else {
      return Math.round(a) + ' ' + unit;
    }
  }

  var RADIUS = 6378137;
  // ringArea function copied from geojson-area
  // (https://github.com/mapbox/geojson-area)
  // This function is distributed under a separate license,
  // see LICENSE.md.
  var ringArea = function ringArea(coords) {
    var rad = function rad(_) {
      return _ * Math.PI / 180;
    };
    var p1, p2, p3, lowerIndex, middleIndex, upperIndex,
      area = 0,
      coordsLength = coords.length;

    if (coordsLength > 2) {
      for (var i = 0; i < coordsLength; i++) {
        if (i === coordsLength - 2) {// i = N-2
          lowerIndex = coordsLength - 2;
          middleIndex = coordsLength -1;
          upperIndex = 0;
        } else if (i === coordsLength - 1) {// i = N-1
          lowerIndex = coordsLength - 1;
          middleIndex = 0;
          upperIndex = 1;
        } else { // i = 0 to N-3
          lowerIndex = i;
          middleIndex = i+1;
          upperIndex = i+2;
        }
        p1 = coords[lowerIndex];
        p2 = coords[middleIndex];
        p3 = coords[upperIndex];
        area += ( rad(p3.lng) - rad(p1.lng) ) * Math.sin( rad(p2.lat));
      }

      area = area * RADIUS * RADIUS / 2;
    }

    return Math.abs(area);
  };
  /**
   * Handles the init hook for polylines and circles.
   * Implements the showOnHover functionality if called for.
   */
  var addInitHook = function() {
    var showOnHover = this.options.measurementOptions && this.options.measurementOptions.showOnHover;
    if (this.options.showMeasurements && !showOnHover) {
      this.showMeasurements();
    }
    if (this.options.showMeasurements && showOnHover) {
      this.on('mouseover', function() {
        this.showMeasurements();
      });
      this.on('mouseout', function() {
        this.hideMeasurements();
      });
    }
  };

  var circleArea = function circleArea(d) {
    var rho = d / RADIUS;
    return 2 * Math.PI * RADIUS * RADIUS * (1 - Math.cos(rho));
  };

  var override = function(method, fn, hookAfter) {
    if (!hookAfter) {
      return function() {
        var originalReturnValue = method.apply(this, arguments);
        var args = Array.prototype.slice.call(arguments)
        args.push(originalReturnValue);
        return fn.apply(this, args);
      }
    } else {
      return function() {
        fn.apply(this, arguments);
        return method.apply(this, arguments);
      }
    }
  };

  L.Polyline.include({
    showMeasurements: function(options) {
      if (!this._map || this._measurementLayer) return this;

      this._measurementOptions = L.extend({
        showOnHover: (options && options.showOnHover) || false,
        minPixelDistance: 30,
        showDistances: true,
        showArea: true,
        showTotalDistance: true,
        lang: {
          totalLength: 'Total length',
          totalArea: 'Total area',
          segmentLength: 'Segment length'
        }
      }, options || {});

      this._measurementLayer = L.layerGroup().addTo(this._map);
      this.updateMeasurements();

      this._map.on('zoomend', this.updateMeasurements, this);

      return this;
    },

    hideMeasurements: function() {
      if (!this._map) return this;

      this._map.off('zoomend', this.updateMeasurements, this);

      if (!this._measurementLayer) return this;
      this._map.removeLayer(this._measurementLayer);
      this._measurementLayer = null;

      return this;
    },

    onAdd: override(L.Polyline.prototype.onAdd, function(originalReturnValue) {
      var showOnHover = this.options.measurementOptions && this.options.measurementOptions.showOnHover;
      if (this.options.showMeasurements && !showOnHover) {
        this.showMeasurements(this.options.measurementOptions);
      }

      return originalReturnValue;
    }),

    onRemove: override(L.Polyline.prototype.onRemove, function(originalReturnValue) {
      this.hideMeasurements();

      return originalReturnValue;
    }, true),

    setLatLngs: override(L.Polyline.prototype.setLatLngs, function(originalReturnValue) {
      this.updateMeasurements();

      return originalReturnValue;
    }),

    spliceLatLngs: override(L.Polyline.prototype.spliceLatLngs, function(originalReturnValue) {
      this.updateMeasurements();

      return originalReturnValue;
    }),

    formatDistance: formatDistance,
    formatArea: formatArea,

    updateMeasurements: function() {
      if (!this._measurementLayer) return this;

      var latLngs = this.getLatLngs(),
        isPolygon = this instanceof L.Polygon,
        options = this._measurementOptions,
        totalDist = 0,
        formatter,
        ll1,
        ll2,
        p1,
        p2,
        pixelDist,
        dist;

      if (latLngs && latLngs.length && L.Util.isArray(latLngs[0])) {
        // Outer ring is stored as an array in the first element,
        // use that instead.
        latLngs = latLngs[0];
      }

      this._measurementLayer.clearLayers();

      if (this._measurementOptions.showDistances && latLngs.length > 1) {
        formatter = this._measurementOptions.formatDistance || L.bind(this.formatDistance, this);

        for (var i = 1, len = latLngs.length; (isPolygon && i <= len) || i < len; i++) {
          ll1 = latLngs[i - 1];
          ll2 = latLngs[i % len];
          dist = ll1.distanceTo(ll2);
          totalDist += dist;

          p1 = this._map.latLngToLayerPoint(ll1);
          p2 = this._map.latLngToLayerPoint(ll2);

          pixelDist = p1.distanceTo(p2);

          if (pixelDist >= options.minPixelDistance) {
            L.marker.measurement(
              this._map.layerPointToLatLng([(p1.x + p2.x) / 2, (p1.y + p2.y) / 2]),
              formatter(dist), options.lang.segmentLength, this._getRotation(ll1, ll2), options)
              .addTo(this._measurementLayer);
          }
        }

        // Show total length for polylines
        if (!isPolygon && this._measurementOptions.showTotalDistance) {
          L.marker.measurement(ll2, formatter(totalDist), options.lang.totalLength, 0, options)
            .addTo(this._measurementLayer);
        }
      }

      if (isPolygon && options.showArea && latLngs.length > 2) {
        formatter = options.formatArea || L.bind(this.formatArea, this);
        var area = ringArea(latLngs);
        L.marker.measurement(this.getBounds().getCenter(),
          formatter(area), options.lang.totalArea, 0, options)
          .addTo(this._measurementLayer);
      }

      return this;
    },

    _getRotation: function(ll1, ll2) {
      var p1 = this._map.project(ll1),
        p2 = this._map.project(ll2);

      return Math.atan((p2.y - p1.y) / (p2.x - p1.x));
    }
  });

  L.Polyline.addInitHook(function() {
    addInitHook.call(this);
  });

  L.Circle.include({
    showMeasurements: function(options) {
      if (!this._map || this._measurementLayer) return this;

      this._measurementOptions = L.extend({
        showOnHover: false,
        showArea: true,
        lang: {
          totalArea: 'Total area',
        }
      }, options || {});

      this._measurementLayer = L.layerGroup().addTo(this._map);
      this.updateMeasurements();

      this._map.on('zoomend', this.updateMeasurements, this);

      return this;
    },

    hideMeasurements: function() {
      if (!this._map) return this;

      this._map.on('zoomend', this.updateMeasurements, this);

      if (!this._measurementLayer) return this;
      this._map.removeLayer(this._measurementLayer);
      this._measurementLayer = null;

      return this;
    },

    onAdd: override(L.Circle.prototype.onAdd, function(originalReturnValue) {
      var showOnHover = this.options.measurementOptions && this.options.measurementOptions.showOnHover;
      if (this.options.showMeasurements && !showOnHover) {
        this.showMeasurements(this.options.measurementOptions);
      }

      return originalReturnValue;
    }),

    onRemove: override(L.Circle.prototype.onRemove, function(originalReturnValue) {
      this.hideMeasurements();

      return originalReturnValue;
    }, true),

    setLatLng: override(L.Circle.prototype.setLatLng, function(originalReturnValue) {
      this.updateMeasurements();

      return originalReturnValue;
    }),

    setRadius: override(L.Circle.prototype.setRadius, function(originalReturnValue) {
      this.updateMeasurements();

      return originalReturnValue;
    }),

    formatArea: formatArea,

    updateMeasurements: function() {
      if (!this._measurementLayer) return;

      var latLng = this.getLatLng(),
        options = this._measurementOptions,
        formatter = options.formatArea || L.bind(this.formatArea, this);

      this._measurementLayer.clearLayers();

      if (options.showArea) {
        formatter = options.formatArea || L.bind(this.formatArea, this);
        var area = circleArea(this.getRadius());
        L.marker.measurement(latLng,
          formatter(area), options.lang.totalArea, 0, options)
          .addTo(this._measurementLayer);
      }
    }
  })

  L.Circle.addInitHook(function() {
    addInitHook.call(this);
  });
})();
